// pgPortalDecal - Implementation of warping code and visual portal effects
/*
	Credits:
		- Code by John "Shambler" Barrett (Shambler@OldUnreal.com, Shambler__@Hotmail.com, ICQ: 108730864)
		- Portal and projectile effects by Fuegerstef
		- Weapon model artwork by Myles Lambert (Myles@lambert.demon.co.uk)
		- Custom crosshair by Kel "Kyllian" Siros (Kyllian@gmail.com)
		- Vehicle teleportation code adapted and improved from code done by VitalOverdose (obtained from the Wiki: http://wiki.beyondunreal.com)
*/

// TODO: This class needs serious cleaning (the growth/shrinking code especially, the warping code can be left messy while you experiment with it)
// N.B. The first step can be to replace all the FMin FMax's with FClamp's

Class pgPortalDecal extends Actor
	config(UnitedMut);

#exec OBJ LOAD File="..\Textures\PortalGunt.utx"
#exec OBJ LOAD File="..\StaticMeshes\PortalGunSM.usx"

// Scripted texture related variables
var ScriptedTexture PortalTex;
var float TexRes;
var config int FramerateDivider;
var int FrameCount;
var int NumPortals;

// Growth/Shrinking variables
var float GrowthTime;
var bool bFinishedGrowth,bOthersCanUse;

// General variables
var pgPortalDecal OtherSide;
var byte FireMode;
var RenderDevice rDev;
var pgPortalGun WeaponOwner;
var vector ForwardDir,StartingLocation;
var float PlaneDepth;
var rotator ReverseRotation;

const PortalRadius=79.f; // Real radius of portal when on drawscale 1.

replication
{
	reliable if (Role == ROLE_Authority)
		FireMode,bOthersCanUse,OtherSide;
}

function PostBeginPlay()
{
	bOthersCanUse = Class'pgPortalGunMut'.Default.bOthersCanUsePortals;
}
static final function bool CheckCorner( vector Offset, vector Start, vector X, Actor A )
{
	return (!A.FastTrace(Start+Offset,Start) || A.FastTrace(Start+Offset,Start+Offset-X*14.f)
		 || !A.FastTrace(Start-Offset,Start) || A.FastTrace(Start-Offset,Start-Offset-X*14.f));
}
static final function bool TryToFit( out vector V, rotator Dir, Actor A, byte FM )
{
	local vector X,Y,Z,HL,HN,StartPos;
	local pgPortalDecal D;

	StartPos = V;
	GetAxes(Dir,X,Y,Z);

	// First see if can push more to center of surface.
	if( A.Trace(HL,HN,V+Y*PortalRadius,V,false)!=None )
		V = HL-Y*PortalRadius;
	if( A.Trace(HL,HN,V-Y*PortalRadius,V,false)!=None )
		V = HL+Y*PortalRadius;
	if( A.Trace(HL,HN,V+Z*PortalRadius,V,false)!=None )
		V = HL-Z*PortalRadius;
	if( A.Trace(HL,HN,V-Z*PortalRadius,V,false)!=None )
		V = HL+Z*PortalRadius;

	// See if we moved too much now and validate were still next to a wall.
	if( VSizeSquared(V-StartPos)>Square(PortalRadius)*0.7f || A.FastTrace(V-X*14.f,V) || !A.FastTrace(V+X*14.f,V) )
		return false;

	// Now validate surface we stand on.
	if( CheckCorner(Y*PortalRadius,V,X,A) || CheckCorner(Z*PortalRadius,V,X,A) )
		return false;

	// Validate were not too close to another portal.
	foreach A.CollidingActors(class'pgPortalDecal',D,PortalRadius*1.9f,V)
	{
		if( (D.ForwardDir Dot X)>0.98f && (D.WeaponOwner!=A || D.FireMode!=FM) ) // Is a coplanar.
			return false;
	}
	return true;
}
simulated final function OpenPortal()
{
	local string sTempStr;
	local FinalBlend MainTex;
	local float ResX, ResY;
	local byte SecondMode;
	local vector X,Y,Z;

	StartingLocation = Location;
	GetAxes(Rotation,X,Y,Z);
	ForwardDir = X;
	PlaneDepth = (X Dot Location);
	ReverseRotation = OrthoRotation(-X,-Y,Z);

	// Search for other portals owned by the same player
	if( Level.NetMode!=NM_Client )
	{
		if( WeaponOwner==None )
		{
			Error("No portal gun owner?");
			return;
		}
		if( FireMode==0 )
			SecondMode = 1;
		else SecondMode = 0;
		if( WeaponOwner.Portals[FireMode]!=None )
			WeaponOwner.Portals[FireMode].Destroy();
		if( WeaponOwner.Portals[SecondMode]!=None )
		{
			WeaponOwner.Portals[SecondMode].OtherSide = Self;
			OtherSide = WeaponOwner.Portals[SecondMode];
		}
		WeaponOwner.Portals[FireMode] = Self;
	}

	// Setup clientside portal textures
	if ( Level.Netmode != NM_DedicatedServer )
	{
		PlaySound(Sound'Portal_OpenG',SLOT_Pain);

		// Grab a reference to the current render device, a hack fix for the UseStencil option breaking either warp zones or drawportal
		rDev = GameEngine(FindObject("Package.GameEngine", Class'GameEngine')).GRenDev;

		// Work out the best scripted texture size (based upon screen resoloution)
		sTempStr = Level.GetLocalPlayerController().ConsoleCommand("GetCurrentRes");

		ResY = InStr(sTempStr, "x");

		// Uses logs to find the 'lowest power of 2' that fits the X/Y resoloution
		ResX = 2 ** (int(loge(float(Left(sTempStr, ResY))) / loge(2.0)) /*+ int(bDoubleRes)*/);
		ResY = 2 ** (int(loge(float(Mid(sTempStr, ResY+1))) / loge(2.0)) /*+ int(bDoubleRes)*/);

		if (ResX < ResY)
			TexRes = ResX;
		else
			TexRes = ResY;

		// Force the other sides texture to update aswel
		if (OtherSide != none)
			OtherSide.PortalTex.Revision++;

		// Setup the actual textures for this portal
		if (PortalTex == none)
			PortalTex = ScriptedTexture(Level.ObjectPool.AllocateObject(Class'ScriptedTexture'));

		PortalTex.Client = self;
		PortalTex.SetSize(TexRes, TexRes);

		// These texture effects provided by Fuegerstef
		Skins[0] = FinalBlend(Level.ObjectPool.AllocateObject(Class'FinalBlend'));

		if (FireMode == 0)
		{
			MainTex = FinalBlend'PortalGunT.PortalPrimary';
			Skins[1] = FinalBlend'PortalGunT.PlasmaPrimary';
		}
		else
		{
			MainTex = FinalBlend'PortalGunT.PortalSecondary';
			Skins[1] = FinalBlend'PortalGunT.PlasmaSecondary';
		}

		FinalBlend(Skins[0]).FrameBufferBlending = MainTex.FrameBufferBlending;
		FinalBlend(Skins[0]).ZWrite = MainTex.ZWrite;
		FinalBlend(Skins[0]).Material = Combiner(Level.ObjectPool.AllocateObject(Class'Combiner'));
		FinalBlend(Skins[0]).FallbackMaterial = FinalBlend(Skins[0]).Material;

		// If your re-using a discarded modifier (from the levels object pool) then it might still retain its old values, reset them all
		FinalBlend(Skins[0]).ZTest = MainTex.ZTest;
		FinalBlend(Skins[0]).AlphaTest = MainTex.AlphaTest;
		FinalBlend(Skins[0]).TwoSided = MainTex.TwoSided;
		FinalBlend(Skins[0]).AlphaRef = MainTex.AlphaRef;

		Combiner(FinalBlend(Skins[0]).Material).CombineOperation = Combiner(MainTex.Material).CombineOperation;
		Combiner(FinalBlend(Skins[0]).Material).Material1 = PortalTex;
		Combiner(FinalBlend(Skins[0]).Material).Material2 = Combiner(MainTex.Material).Material2;
		Combiner(FinalBlend(Skins[0]).Material).Mask = Combiner(MainTex.Material).Mask;

		Combiner(FinalBlend(Skins[0]).Material).AlphaOperation = Combiner(MainTex.Material).AlphaOperation;
		Combiner(FinalBlend(Skins[0]).Material).InvertMask = Combiner(MainTex.Material).InvertMask;
		Combiner(FinalBlend(Skins[0]).Material).Modulate2X = Combiner(MainTex.Material).Modulate2X;
		Combiner(FinalBlend(Skins[0]).Material).Modulate4X = Combiner(MainTex.Material).Modulate4X;

		Skins[2] = Texture'PortalGunT.CollisionInvisible';

		PortalTex.Revision++;
	}

	// Start with a drawscale of 0 and make the portal 'grow', so as to make the portal seem to appear out of nothing
	SetDrawScale(0);
	Enable('Tick');
}
simulated function PostNetBeginPlay()
{
	if( Level.NetMode==NM_Client )
		OpenPortal();
}

// Update texture size and framerate based upon the number of portals in the map (needs tweaking as FPS can still crawl with a lot of portals)
simulated function UpdateList(bool bNewPortal)
{
	local int NewSize;

	NumPortals += 1 - (2 * int(bNewPortal));

	if (PortalTex != none)
	{
		if (NumPortals >= 2)
		{
			NewSize = TexRes / (NumPortals / 2);
			PortalTex.SetSize(NewSize, NewSize);

			FramerateDivider = default.FramerateDivider * (NumPortals / 2);
		}
		else
		{
			PortalTex.SetSize(TexRes, TexRes);
			FramerateDivider = default.FramerateDivider;
		}
	}
}

simulated final function UpdateGrowth( float DeltaTime )
{
	SetDrawScale(FMin(1.f,DrawScale + (DeltaTime/GrowthTime)));
	if( DrawScale>=1.f )
		EndGrowth();
}

// Handle scripted texture ticking and portal growth
simulated function Tick(float DeltaTime)
{
	// Portal is still growing/shrinking (TODO: If the portal growth fails during this tick then find a way to determine the maximum portal size instead of using the last drawscale)
	if (!bFinishedGrowth)
		UpdateGrowth(DeltaTime);

	// Scripted texture ticking
	if (Level.Netmode != NM_DedicatedServer)
	{
		if (FrameCount > FramerateDivider)
		{
			PortalTex.Revision++;
			FrameCount = 0;
		}
		FrameCount++;
	}

	if( Level.NetMode!=NM_Client && Base!=None && StartingLocation!=Location )
		Destroy(); // Base moved, kill portal.
}

// Stops all growth/shrinking
simulated final function EndGrowth()
{
	bFinishedGrowth = True;

	if( Level.Netmode == NM_DedicatedServer && Base==None )
		Disable('Tick');
}

simulated event RenderTexture(ScriptedTexture Tex)
{
	local string sInitVal;
	local rotator ExtraAngle;
	local PlayerController PC;

	PC = Level.GetLocalPlayerController();

	// Hopefully this will improve fps
	if ( VSizeSquared(PC.CalcViewLocation-Location)>2250000.f || !FastTrace(PC.CalcViewLocation,Location) || OtherSide==None )
		return;

	// Lovely hack for scripted textures :) (without this, either warp zones would break or portals would break)
	sInitVal = rDev.GetPropertyText("UseStencil");
	rDev.SetPropertyText("UseStencil", "False");

	if( Abs(ForwardDir.Z)<0.4f && Abs(OtherSide.ForwardDir.Z)<0.4f )
	{
		// Benjamin
		ExtraAngle.Yaw = rotator(PC.CalcViewLocation - Location).Yaw - Rotation.Yaw;
	}

	Tex.DrawPortal(0, 0, TexRes, TexRes, Level, OtherSide.Location, OtherSide.Rotation + ExtraAngle, 90, False);

	rDev.SetPropertyText("UseStencil", sInitVal);
}

// Make sure this is destroyed on round resets in Onslaught and Assault
function Reset()
{
	Destroy();
}

// Free the allocated textures and tell all portals that there is one less portal
simulated function Destroyed()
{
	if (PortalTex != none)
		Level.ObjectPool.FreeObject(PortalTex);

	if (Skins.Length > 0 && FinalBlend(Skins[0]) != none)
	{
		if (Combiner(FinalBlend(Skins[0]).Material) != none)
			Level.ObjectPool.FreeObject(FinalBlend(Skins[0]).Material);

		Level.ObjectPool.FreeObject(Skins[0]);
	}

	if( Level.NetMode!=NM_DedicatedServer )
	{
		PlaySound(Sound'Portal_CloseG',SLOT_Pain);
		Spawn(Class'pgProjImpactEmitter').SetColour(FireMode);
	}

	Super.Destroyed();
}


// Handle actor warping

simulated function Touch(actor Other)
{
	if (Other == none)
		return;

	// If attempting to warp another portal projectile of the same owner, use it to either increase or decrease this portals size :) (based on wether its a primary or secondary proj)
	if( pgPortalProjectile(Other)!=none )
		return;

	if (OtherSide != none && OtherSide.Base != Other && ZoneInfo(Other) == none && Volume(Other) == none && Mover(Other) == none && GameObject(Other) == none )
	{
		if( Other.GetRenderBoundingSphere().W<GetRenderBoundingSphere().W )
			WarpActor(Other);
	}
}

// ??
simulated function Attach(Actor Other)
{
	Touch(Other);
}

simulated function Bump(Actor Other)
{
	Touch(Other);
}

simulated singular function WarpActor(actor Other)
{
	local vector WarpPos,OldPos;
	local rotator WarpRot;
	local pgWarpPawnHandle W;
	local bool bIsKarma;

	if( OtherSide==None )
		return;

	// So the code below looks a little more elegant ;)
	if( Other.Physics == PHYS_Karma )
		bIsKarma = True;

	if( (Pawn(Other)!=None && Monster(Other)==None && Other!=Instigator && !bOthersCanUse)
		 || (Other.bJustTeleported && !bIsKarma) || Other.Physics==PHYS_KarmaRagdoll )
		return;

	if (Pawn(Other) != none)
	{
		if( Level.NetMode==NM_Client )
		{
			if( Pawn(Other).IsLocallyControlled() ) // Prevent client side landing noise.
			{
				Other.Disable('Touch');
				Other.Disable('UnTouch');
				Other.Disable('Landed');
				Other.Disable('HitWall');
				Other.SetPhysics(PHYS_None);
				Other.PendingTouch = Self;
			}
			return;
		}
		// Hack test. TODO: replace with better method (maybe use Inventory?)
		foreach DynamicActors(Class'pgWarpPawnHandle',W)
			if( W.Owner==Other )
				return;
	}
	Other.Disable('Touch');
	Other.Disable('UnTouch');
	Disable('Touch');
	OtherSide.Disable('Touch');
	WarpToCoordsPos(WarpPos,Other);

	if(Pawn(Other) != None && Vehicle(Other) == none)
		WarpRot = Pawn(Other).GetViewRotation();
	else WarpRot = Other.Rotation;

	WarpToCoordsRot(WarpRot);
	OldPos = Other.Location;

	if (Pawn(Other) != none)
	{
		Pawn(Other).bWarping = False;

		SetCollision(False, False, False);

		if( Other.SetLocation(WarpPos) )
		{
			WarpPos = Other.Location;
			Other.SetLocation(OldPos);
			W = Spawn(Class'pgWarpPawnHandle',Other);
			W.SetPortal(Self);
			W.DesiredPosition = WarpPos;
		}
		SetCollision(True, False, False);
		Other.Enable('Touch');
		Other.Enable('UnTouch');
	}
	else
	{
		PlaySound(Sound'Portal_EntryG');
		OtherSide.PlaySound(Sound'Portal_ExitG');
		Other.Disable('Landed');
		Other.Disable('HitWall');
		Other.SetLocation(WarpPos);
		Other.SetRotation(WarpRot);
		WarpToCoordsAxis(Other.Velocity);
		Other.PendingTouch = Self;
	}

	Enable('Touch');
	OtherSide.Enable('Touch');
}

// Prevent projectiles from hitting wall behind portal.
simulated function PostTouch( Actor Other )
{
	Other.Enable('Landed');
	Other.Enable('HitWall');
	Other.Enable('Touch');
	Other.Enable('UnTouch');
}

simulated final function float BoxPushOut( Actor Other )
{
	return Abs(ForwardDir.Z*Other.CollisionHeight)+Abs(ForwardDir.Y*Other.CollisionRadius)+Abs(ForwardDir.X*Other.CollisionRadius);
}
simulated final function WarpToCoordsRot( out rotator R )
{
	local vector X,Y,Z;

	// Complex method.
	GetAxes(R,X,Y,Z);
	WarpToCoordsAxis(X);
	WarpToCoordsAxis(Z);
	Y = (Z Cross X);
	R = Normalize(OrthoRotation(X,Y,Z));
}
simulated final function WarpToCoordsPos( out vector V, Actor Other )
{
	V = Other.Location-Location;
	WarpToCoordsAxis(V);
	V = (V-(V Dot OtherSide.ForwardDir)*OtherSide.ForwardDir); // Remove depth axis.
	V+=OtherSide.Location+OtherSide.ForwardDir*(OtherSide.BoxPushOut(Other)+15.f);
}
simulated final function WarpToCoordsPoint( out vector V )
{
	V-=Location;
	WarpToCoordsAxis(V);
	V = (V-(V Dot OtherSide.ForwardDir)*OtherSide.ForwardDir); // Remove depth axis.
	V+=OtherSide.Location+OtherSide.ForwardDir*15.f;
}
simulated final function WarpToCoordsAxis( out vector V )
{
	V = (V << Rotation) >> OtherSide.ReverseRotation;
}

defaultproperties
{
     FramerateDivider=4
     GrowthTime=0.1500000
     DrawType=DT_StaticMesh
     StaticMesh=StaticMesh'PortalGunSM.MeshPortal'
     RemoteRole=ROLE_SimulatedProxy
     Style=STY_Additive
     bUnlit=True
     TransientSoundVolume=1.5
	TransientSoundRadius=400
     bCollideActors=True
	AmbientSound=Sound'portal_ambient_loop'
	SoundVolume=255
	SoundRadius=24
	bReplicateInstigator=true
}
